package com.jeeex.objregex.impl;

import static com.google.common.base.Preconditions.checkNotNull;
import static com.jeeex.objregex.impl.TransitionIdentifier.BOF;
import static com.jeeex.objregex.impl.TransitionIdentifier.EOF;
import static com.jeeex.objregex.impl.TransitionIdentifier.EPSILON;
import static java.text.MessageFormat.format;

import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentMap;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Function;
import com.google.common.base.Predicate;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.MapMaker;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.jeeex.objregex.ObjectPattern;
import com.jeeex.objregex.javacc.ASTStart;

/**
 * An implementation of {@link ObjectPattern}.
 * <p>
 * The users of objregex should not initiate this class directly -
 * {@link DefaultRegexFactory} should be used instead.
 * 
 * @author Jeeyoung Kim
 * @since 2010-02-14
 * 
 */
class ObjectPatternImpl<T> implements ObjectPattern<T> {
	static enum IdentifierCategory {
		/**
		 * This identifier is a key in {@link ObjectPatternImpl#idToPredicate}
		 */
		PREDICATE,
		/**
		 * This identifier is a key in {@link ObjectPatternImpl#idToPattern}
		 */
		PATTERN,
		/**
		 * Belongs in neither.
		 */
		UNKNOWN;
	}

	@VisibleForTesting
	final Map<String, Predicate<T>> idToPredicate = Maps.newHashMap();
	@VisibleForTesting
	final Map<String, String> idToPattern = Maps.newHashMap();
	@VisibleForTesting
	final Set<String> assignedIds = Sets.newHashSet();
	private final String regex;

	/**
	 * Visitor to translate the AST to {@link State}s.
	 */
	private final ASTVisitor visitor = new ASTVisitor();
	private final SingleLazyStateManager manager = new SingleLazyStateManager() {
		@Override
		public void initializeLazySingle(LazyState tail,
				TransitionIdentifier identifier, LeafState head) {
			// lazy initialization logic is different for each category of
			// identifiers.
			switch (categorize(identifier)) {
			case PREDICATE:
				// in case of PREDICATE, transition from tail to
				// head is created.
				tail.addTransition(identifier, head);
				break;
			case PATTERN:
				// TODO(Jeeyoung Kim) - Cache the parse tree.

				// in case of PATTERN, a new State is generated by parsing
				// the pattern string, then it is connected to tail and
				// head.
				String pattern = idToPattern.get(identifier.getId());
				State state = visitor.start(COMPILED_ASTS.get(pattern), this);

				// connect initialized state to head and tail.
				tail.addTransition(EPSILON, state.getTail());
				state.getHead().addTransition(EPSILON, head);
				break;
			case UNKNOWN:
				throw new RuntimeException(format("Unknown identifier {0}.",
						identifier));
			}
		}
	};

	/**
	 * State graph generated from the string {@link #regex}.
	 */
	private State state;

	/**
	 * Map of "regex pattern" -> "Compiled AST"
	 */
	private static final ConcurrentMap<String, ASTStart> COMPILED_ASTS = new MapMaker()
			.softKeys().softValues().makeComputingMap(
					new Function<String, ASTStart>() {
						public ASTStart apply(String pattern) {
							return RegexUtil.getRootNode(pattern);
						}
					});;

	ObjectPatternImpl(String regex) {
		this.regex = regex;
	}

	public boolean apply(List<? extends T> input) {
		return match(input);
	}

	private IdentifierCategory categorize(TransitionIdentifier identifier) {
		String idStr = identifier.getId();
		if (idToPattern.containsKey(idStr)) {
			return IdentifierCategory.PATTERN;
		}
		if (idToPredicate.containsKey(idStr)) {
			return IdentifierCategory.PREDICATE;
		}
		return IdentifierCategory.UNKNOWN;
	}

	public void compile() {
		State state = visitor.start(COMPILED_ASTS.get(regex), manager);
		// finished - set up stuff.
		this.state = state;
	}

	/**
	 * Consume a token of input
	 * 
	 * @param states
	 *            Starting set of the NFA states
	 * @param token
	 *            Token to be consumed.
	 * @return Set of NFA states after {@code token} has been consumed. That is,
	 *         set of all states that is connected by a
	 *         {@link TransitionIdentifier} that evaluates {@code token} to
	 *         True.
	 */
	Set<State> consume(final Set<State> states, T token) {
		// set of all transition id's going out from the states.
		final Set<TransitionIdentifier> outgoingTransitions = StateUtil
				.getOutgoingTransitions(states);
		final ImmutableSet.Builder<State> nextStatesBuilder = ImmutableSet
				.builder();

		for (TransitionIdentifier tid : outgoingTransitions) {
			if (tid.isSpecial()) {
				// Special TransitionIdentifiers are not mapped to any
				// predicates.
				continue;
			} else if (idToPredicate.get(tid.getId()).apply(token) != tid
					.isNegation()) {
				// if the predicate evaluates to true, than traverse the
				// sets.
				nextStatesBuilder.addAll(StateUtil.traverse(states, tid));
			}
		}
		return nextStatesBuilder.build();
	}

	public String getRegex() {
		return regex;
	}

	public boolean match(List<? extends T> input) throws NullPointerException {

		// temporary, current set of states reached by the regex engine.
		// starts from the transitive closure of state.getTail().
		Set<State> currentStates = StateUtil.transitiveClosure(ImmutableSet
				.of(state.getTail()), ImmutableSet.of(EPSILON, BOF));

		for (T curToken : input) {
			// consume the token, and take the transitive closure.
			currentStates = StateUtil.transitiveClosure(consume(currentStates,
					curToken));
			// states cannot grow if it's empty, so terminate the loop.
			if (currentStates.isEmpty()) {
				break;
			}
		}

		currentStates = StateUtil.transitiveClosure(currentStates, ImmutableSet
				.of(EPSILON, EOF));
		return currentStates.contains(state.getHead());
	}

	public void set(String identifier, Predicate<T> predicate)
			throws NullPointerException {
		checkNotNull(identifier);
		checkNotNull(predicate);

		unset(identifier);
		idToPredicate.put(identifier, predicate);
		assignedIds.add(identifier);
	}

	public void set(String identifier, String pattern)
			throws NullPointerException {
		checkNotNull(identifier);
		checkNotNull(pattern);

		unset(identifier);
		idToPattern.put(identifier, pattern);
		assignedIds.add(identifier);
	}

	/**
	 * Unset the given identifier from {@link #idToPattern} and
	 * {@link #idToPredicate}.
	 * 
	 * @param identifier
	 */
	public void unset(String identifier) {
		idToPattern.remove(identifier);
		idToPredicate.remove(identifier);
		assignedIds.remove(identifier);
	}

	/**
	 * Returns the set of all assigned ids. The returned set is unmodifiable.
	 * 
	 * @return
	 */
	public Set<String> getAssignedIds() {
		return Collections.unmodifiableSet(assignedIds);
	}
}
